home *** CD-ROM | disk | FTP | other *** search
/ PC Format (PL) 2008 February / PC_Format_022008.iso / Internet / Mozilla Thunderbird wtyczki / lightning-0.7-tb-win.xpi / components / calICSCalendar.js < prev    next >
Encoding:
Text File  |  2007-09-22  |  37.4 KB  |  1,055 lines

  1. /* -*- Mode: java; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
  2. /* ***** BEGIN LICENSE BLOCK *****
  3.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  4.  *
  5.  * The contents of this file are subject to the Mozilla Public License Version
  6.  * 1.1 (the "License"); you may not use this file except in compliance with
  7.  * the License. You may obtain a copy of the License at
  8.  * http://www.mozilla.org/MPL/
  9.  *
  10.  * Software distributed under the License is distributed on an "AS IS" basis,
  11.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  12.  * for the specific language governing rights and limitations under the
  13.  * License.
  14.  *
  15.  * The Original Code is mozilla calendar code.
  16.  *
  17.  * The Initial Developer of the Original Code is
  18.  *   Michiel van Leeuwen <mvl@exedo.nl>
  19.  * Portions created by the Initial Developer are Copyright (C) 2004
  20.  * the Initial Developer. All Rights Reserved.
  21.  *
  22.  * Contributor(s):
  23.  *   Vladimir Vukicevic <vladimir.vukicevic@oracle.com>
  24.  *   Dan Mosedale <dan.mosedale@oracle.com>
  25.  *   Joey Minta <jminta@gmail.com>
  26.  *
  27.  * Alternatively, the contents of this file may be used under the terms of
  28.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  29.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  30.  * in which case the provisions of the GPL or the LGPL are applicable instead
  31.  * of those above. If you wish to allow use of your version of this file only
  32.  * under the terms of either the GPL or the LGPL, and not to allow others to
  33.  * use your version of this file under the terms of the MPL, indicate your
  34.  * decision by deleting the provisions above and replace them with the notice
  35.  * and other provisions required by the GPL or the LGPL. If you do not delete
  36.  * the provisions above, a recipient may use your version of this file under
  37.  * the terms of any one of the MPL, the GPL or the LGPL.
  38.  *
  39.  * ***** END LICENSE BLOCK ***** */
  40.  
  41. //
  42. // calICSCalendar.js
  43. //
  44. // This is a non-sync ics file. It reads the file pointer to by uri when set,
  45. // then writes it on updates. External changes to the file will be
  46. // ignored and overwritten.
  47. //
  48. // XXX Should do locks, so that external changes are not overwritten.
  49.  
  50. const CI = Components.interfaces;
  51. const calIOperationListener = Components.interfaces.calIOperationListener;
  52. const calICalendar = Components.interfaces.calICalendar;
  53. const calIErrors = Components.interfaces.calIErrors;
  54.  
  55. var appInfo = Components.classes["@mozilla.org/xre/app-info;1"].
  56.                          getService(Components.interfaces.nsIXULAppInfo);
  57. var isOnBranch = appInfo.platformVersion.indexOf("1.8") == 0;
  58.  
  59. function calICSCalendar () {
  60.     this.wrappedJSObject = this;
  61.     this.initICSCalendar();
  62.  
  63.     this.unmappedComponents = [];
  64.     this.unmappedProperties = [];
  65.     this.queue = new Array();
  66. }
  67.  
  68. calICSCalendar.prototype = {
  69.     mICSService: null,
  70.     mObserver: null,
  71.     locked: false,
  72.  
  73.     QueryInterface: function (aIID) {
  74.         if (!aIID.equals(Components.interfaces.nsISupports) &&
  75.             !aIID.equals(Components.interfaces.calICalendarProvider) &&
  76.             !aIID.equals(Components.interfaces.calICalendar) &&
  77.             !aIID.equals(Components.interfaces.nsIStreamListener) &&
  78.             !aIID.equals(Components.interfaces.nsIStreamLoaderObserver) &&
  79.             !aIID.equals(Components.interfaces.nsIInterfaceRequestor)) {
  80.             throw Components.results.NS_ERROR_NO_INTERFACE;
  81.         }
  82.  
  83.         return this;
  84.     },
  85.     
  86.     initICSCalendar: function() {
  87.         this.mMemoryCalendar = Components.classes["@mozilla.org/calendar/calendar;1?type=memory"]
  88.                                          .createInstance(Components.interfaces.calICalendar);
  89.         this.mICSService = Components.classes["@mozilla.org/calendar/ics-service;1"]
  90.                                      .getService(Components.interfaces.calIICSService);
  91.  
  92.         this.mObserver = new calICSObserver(this);
  93.         this.mMemoryCalendar.addObserver(this.mObserver);
  94.         this.mMemoryCalendar.wrappedJSObject.calendarToReturn = this;
  95.     },
  96.  
  97.     //
  98.     // calICalendarProvider interface
  99.     //
  100.     get prefChromeOverlay() {
  101.         return null;
  102.     },
  103.  
  104.     get displayName() {
  105.         return calGetString("calendar", "icsName");
  106.     },
  107.  
  108.     createCalendar: function ics_createCal() {
  109.         throw NS_ERROR_NOT_IMPLEMENTED;
  110.     },
  111.  
  112.     deleteCalendar: function ics_deleteCal(cal, listener) {
  113.         throw NS_ERROR_NOT_IMPLEMENTED;
  114.     },
  115.  
  116.     //
  117.     // calICalendar interface
  118.     //
  119.     // attribute AUTF8String id;
  120.     mID: null,
  121.     get id() {
  122.         return this.mID;
  123.     },
  124.     set id(id) {
  125.         if (this.mID)
  126.             throw Components.results.NS_ERROR_ALREADY_INITIALIZED;
  127.         return (this.mID = id);
  128.     },
  129.  
  130.     get name() {
  131.         return getCalendarManager().getCalendarPref(this, "NAME");
  132.     },
  133.     set name(name) {
  134.         getCalendarManager().setCalendarPref(this, "NAME", name);
  135.     },
  136.  
  137.     get type() { return "ics"; },
  138.  
  139.     mReadOnly: false,
  140.  
  141.     get readOnly() { 
  142.         return this.mReadOnly;
  143.     },
  144.     set readOnly(bool) {
  145.         this.mReadOnly = bool;
  146.     },
  147.  
  148.     get canRefresh() {
  149.         return true;
  150.     },
  151.  
  152.     mUri: null,
  153.     get uri() { return this.mUri },
  154.     set uri(aUri) {
  155.         this.mUri = aUri;
  156.         this.mMemoryCalendar.uri = this.mUri;
  157.  
  158.         // Use the ioservice, to create a channel, which makes finding the
  159.         // right hooks to use easier.
  160.         var ioService = Components.classes["@mozilla.org/network/io-service;1"]
  161.                                   .getService(Components.interfaces.nsIIOService);
  162.         var channel = ioService.newChannelFromURI(this.mUri);
  163.  
  164.         if (channel instanceof Components.interfaces.nsIHttpChannel) {
  165.             this.mHooks = new httpHooks();
  166.         } else {
  167.             this.mHooks = new dummyHooks();
  168.         }
  169.  
  170.         this.refresh();
  171.     },
  172.  
  173.     refresh: function calICSCalendar_refresh() {
  174.         this.queue.push({action: 'refresh'});
  175.         this.processQueue();
  176.     },
  177.  
  178.     doRefresh: function calICSCalendar_doRefresh() {
  179.         var ioService = Components.classes["@mozilla.org/network/io-service;1"]
  180.                                   .getService(Components.interfaces.nsIIOService);
  181.  
  182.         var channel = ioService.newChannelFromURI(this.mUri);
  183.         channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
  184.         channel.notificationCallbacks = this;
  185.  
  186.         // Allow the hook to do its work, like a performing a quick check to
  187.         // see if the remote file really changed. Might save a lot of time
  188.         this.mHooks.onBeforeGet(channel);
  189.  
  190.         var streamLoader = Components.classes["@mozilla.org/network/stream-loader;1"]
  191.                                      .createInstance(Components.interfaces.nsIStreamLoader);
  192.  
  193.         // Lock other changes to the item list.
  194.         this.lock();
  195.  
  196.         try {
  197.             if (isOnBranch) {
  198.                 streamLoader.init(channel, this, this);
  199.             } else {
  200.                 streamLoader.init(this);
  201.                 channel.asyncOpen(streamLoader, this);
  202.             }
  203.         } catch(e) {
  204.             // File not found: a new calendar. No problem.
  205.             this.unlock();
  206.         }
  207.     },
  208.  
  209.     calendarPromotedProps: {
  210.         "PRODID": true,
  211.         "VERSION": true
  212.     },
  213.  
  214.     // nsIStreamLoaderObserver impl
  215.     // Listener for download. Parse the downloaded file
  216.  
  217.     onStreamComplete: function(loader, ctxt, status, resultLength, result)
  218.     {
  219.         // No need to do anything if there was no result
  220.         if (!resultLength) {
  221.             this.unlock();
  222.             return;
  223.         }
  224.         
  225.         // Allow the hook to get needed data (like an etag) of the channel
  226.         var cont = this.mHooks.onAfterGet();
  227.         if (!cont) {
  228.             this.unlock();
  229.             return;
  230.         }
  231.  
  232.         // This conversion is needed, because the stream only knows about
  233.         // byte arrays, not about strings or encodings. The array of bytes
  234.         // need to be interpreted as utf8 and put into a javascript string.
  235.         var unicodeConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
  236.                                          .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
  237.         // ics files are always utf8
  238.         unicodeConverter.charset = "UTF-8";
  239.         var str;
  240.         try {
  241.             str = unicodeConverter.convertFromByteArray(result, result.length);
  242.         } catch(e) {
  243.             this.mObserver.onError(calIErrors.CAL_UTF8_DECODING_FAILED, e.toString());
  244.             this.unlock();
  245.             return;
  246.         }
  247.  
  248.         // Create a new calendar, to get rid of all the old events
  249.         this.mMemoryCalendar = Components.classes["@mozilla.org/calendar/calendar;1?type=memory"]
  250.                                          .createInstance(Components.interfaces.calICalendar);
  251.         this.mMemoryCalendar.uri = this.mUri;
  252.         this.mMemoryCalendar.wrappedJSObject.calendarToReturn = this;
  253.  
  254.         this.mObserver.onStartBatch();
  255.  
  256.         // Wrap parsing in a try block. Will ignore errors. That's a good thing
  257.         // for non-existing or empty files, but not good for invalid files.
  258.         // That's why we put them in readOnly mode
  259.         try {
  260.             var parser = Components.classes["@mozilla.org/calendar/ics-parser;1"].
  261.                                     createInstance(Components.interfaces.calIIcsParser);
  262.             parser.parseString(str);
  263.             var items = parser.getItems({});
  264.             
  265.             for each (var item in items) {
  266.                 this.mMemoryCalendar.adoptItem(item, null);
  267.             }
  268.             this.unmappedComponents = parser.getComponents({});
  269.             this.unmappedProperties = parser.getProperties({});
  270.         } catch(e) {
  271.             LOG("Parsing the file failed:"+e);
  272.             this.mObserver.onError(e.result, e.toString());
  273.         }
  274.         this.mObserver.onEndBatch();
  275.         this.mObserver.onLoad(this);
  276.         
  277.         // Now that all items have been stuffed into the memory calendar
  278.         // we should add ourselves as observer. It is important that this
  279.         // happens *after* the calls to adoptItem in the above loop to prevent
  280.         // the views from being notified.
  281.         this.mMemoryCalendar.addObserver(this.mObserver);
  282.         
  283.         this.unlock();
  284.     },
  285.  
  286.     writeICS: function () {
  287.         this.lock();
  288.         try {
  289.             if (!this.mUri)
  290.                 throw Components.results.NS_ERROR_FAILURE;
  291.             // makeBackup will call doWriteICS
  292.             this.makeBackup(this.doWriteICS);
  293.         } catch (exc) {
  294.             this.unlock();
  295.             throw exc;
  296.         }
  297.     },
  298.  
  299.     doWriteICS: function () {
  300.         var savedthis = this;
  301.         var appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"]
  302.                                    .getService(Components.interfaces.nsIAppStartup);
  303.         var listener =
  304.         {
  305.             serializer: null,
  306.             onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail)
  307.             {
  308.                 var inLastWindowClosingSurvivalArea = false;
  309.                 try  {
  310.                     // All events are returned. Now set up a channel and a
  311.                     // streamloader to upload.  onStopRequest will be called
  312.                     // once the write has finished
  313.                     var ioService = Components.classes
  314.                         ["@mozilla.org/network/io-service;1"]
  315.                         .getService(Components.interfaces.nsIIOService);
  316.                     var channel = ioService.newChannelFromURI(savedthis.mUri);
  317.  
  318.                     // Allow the hook to add things to the channel, like a
  319.                     // header that checks etags
  320.                     savedthis.mHooks.onBeforePut(channel);
  321.  
  322.                     channel.notificationCallbacks = savedthis;
  323.                     var uploadChannel = channel.QueryInterface(
  324.                         Components.interfaces.nsIUploadChannel);
  325.  
  326.                     // Serialize
  327.                     var icsStream = this.serializer.serializeToInputStream();
  328.  
  329.                     // Upload
  330.                     uploadChannel.setUploadStream(icsStream,
  331.                                                   "text/calendar", -1);
  332.  
  333.                     appStartup.enterLastWindowClosingSurvivalArea();
  334.                     inLastWindowClosingSurvivalArea = true;
  335.                     channel.asyncOpen(savedthis, savedthis);
  336.                 } catch (ex) {
  337.                     if (inLastWindowClosingSurvivalArea) {
  338.                         appStartup.exitLastWindowClosingSurvivalArea();
  339.                     }
  340.                     savedthis.mObserver.onError(
  341.                         ex.result, "The calendar could not be saved; there " +
  342.                         "was a failure: 0x" + ex.result.toString(16));
  343.                     savedthis.unlock();
  344.                 }
  345.             },
  346.             onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems)
  347.             {
  348.                 this.serializer.addItems(aItems, aCount);
  349.             }
  350.         };
  351.         listener.serializer = Components.classes["@mozilla.org/calendar/ics-serializer;1"].
  352.                                          createInstance(Components.interfaces.calIIcsSerializer);
  353.         for each (var comp in this.unmappedComponents) {
  354.             listener.serializer.addComponent(comp);
  355.         }
  356.         for each (var prop in this.unmappedProperties) {
  357.             listener.serializer.addProperty(prop);
  358.         }
  359.  
  360.         // don't call this.getItems, because we are locked:
  361.         this.mMemoryCalendar.getItems(calICalendar.ITEM_FILTER_TYPE_ALL | calICalendar.ITEM_FILTER_COMPLETED_ALL,
  362.                                       0, null, null, listener);
  363.     },
  364.  
  365.     // nsIStreamListener impl
  366.     // For after publishing. Do error checks here
  367.     onStartRequest: function(request, ctxt) {},
  368.     onDataAvailable: function(request, ctxt, inStream, sourceOffset, count) {
  369.          // All data must be consumed. For an upload channel, there is
  370.          // no meaningfull data. So it gets read and then ignored
  371.          var scriptableInputStream = 
  372.              Components.classes["@mozilla.org/scriptableinputstream;1"]
  373.                        .createInstance(Components.interfaces.nsIScriptableInputStream);
  374.          scriptableInputStream.init(inStream);
  375.          scriptableInputStream.read(-1);
  376.     },
  377.     onStopRequest: function(request, ctxt, status, errorMsg)
  378.     {
  379.         ctxt = ctxt.wrappedJSObject;
  380.         var channel;
  381.         try {
  382.             channel = request.QueryInterface(Components.interfaces.nsIHttpChannel);
  383.             LOG("calICSCalendar channel.requestSucceeded: " + channel.requestSucceeded);
  384.         } catch(e) {
  385.         }
  386.  
  387.         if (channel && !channel.requestSucceeded) {
  388.             ctxt.mObserver.onError(channel.requestSucceeded,
  389.                                    "Publishing the calendar file failed\n" +
  390.                                        "Status code: "+channel.responseStatus+": "+channel.responseStatusText+"\n");
  391.         }
  392.  
  393.         else if (!channel && !Components.isSuccessCode(request.status)) {
  394.             ctxt.mObserver.onError(request.status,
  395.                                    "Publishing the calendar file failed\n" +
  396.                                        "Status code: "+request.status.toString(16)+"\n");
  397.         }
  398.  
  399.         // Allow the hook to grab data of the channel, like the new etag
  400.         ctxt.mHooks.onAfterPut(channel);
  401.  
  402.         ctxt.unlock();
  403.         var appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"]
  404.                                    .getService(Components.interfaces.nsIAppStartup);
  405.         appStartup.exitLastWindowClosingSurvivalArea();
  406.     },
  407.  
  408.     addObserver: function (aObserver) {
  409.         this.mObserver.addObserver(aObserver);
  410.     },
  411.     removeObserver: function (aObserver) {
  412.         this.mObserver.removeObserver(aObserver);
  413.     },
  414.  
  415.     get sendItipInvitations() { return true; },
  416.  
  417.     // Always use the queue, just to reduce the amount of places where
  418.     // this.mMemoryCalendar.addItem() and friends are called. less
  419.     // copied code.
  420.     addItem: function (aItem, aListener) {
  421.         if (this.readOnly) 
  422.             throw Components.interfaces.calIErrors.CAL_IS_READONLY;
  423.         this.queue.push({action:'add', item:aItem, listener:aListener});
  424.         this.processQueue();
  425.     },
  426.  
  427.     modifyItem: function (aNewItem, aOldItem, aListener) {
  428.         if (this.readOnly) 
  429.             throw Components.interfaces.calIErrors.CAL_IS_READONLY;
  430.         this.queue.push({action:'modify', oldItem: aOldItem,
  431.                          newItem: aNewItem, listener:aListener});
  432.         this.processQueue();
  433.     },
  434.  
  435.     deleteItem: function (aItem, aListener) {
  436.         if (this.readOnly) 
  437.             throw Components.interfaces.calIErrors.CAL_IS_READONLY;
  438.         this.queue.push({action:'delete', item:aItem, listener:aListener});
  439.         this.processQueue();
  440.     },
  441.  
  442.     getItem: function (aId, aListener) {
  443.         this.queue.push({action:'get_item', id:aId, listener:aListener});
  444.         this.processQueue();
  445.     },
  446.  
  447.     getItems: function (aItemFilter, aCount,
  448.                         aRangeStart, aRangeEnd, aListener)
  449.     {
  450.         this.queue.push({action:'get_items',
  451.                          itemFilter:aItemFilter, count:aCount,
  452.                          rangeStart:aRangeStart, rangeEnd:aRangeEnd,
  453.                          listener:aListener});
  454.         this.processQueue();
  455.     },
  456.  
  457.     processQueue: function ()
  458.     {
  459.         if (this.isLocked())
  460.             return;
  461.         var a;
  462.         var writeICS = false;
  463.         var refreshAction = null;
  464.         while ((a = this.queue.shift())) {
  465.             switch (a.action) {
  466.                 case 'add':
  467.                     this.mMemoryCalendar.addItem(a.item, a.listener);
  468.                     writeICS = true;
  469.                     break;
  470.                 case 'modify':
  471.                     this.mMemoryCalendar.modifyItem(a.newItem, a.oldItem,
  472.                                                     a.listener);
  473.                     writeICS = true;
  474.                     break;
  475.                 case 'delete':
  476.                     this.mMemoryCalendar.deleteItem(a.item, a.listener);
  477.                     writeICS = true;
  478.                     break;
  479.                 case 'get_item':
  480.                     this.mMemoryCalendar.getItem(a.id, a.listener);
  481.                     break;
  482.                 case 'get_items':
  483.                     this.mMemoryCalendar.getItems(a.itemFilter, a.count,
  484.                                                   a.rangeStart, a.rangeEnd,
  485.                                                   a.listener);
  486.                     break;
  487.                 case 'refresh':
  488.                     refreshAction = a;
  489.                     break;
  490.             }
  491.             if (refreshAction) {
  492.                 // break queue processing here and wait for refresh to finish
  493.                 // before processing further operations
  494.                 break;
  495.             }
  496.         }
  497.         if (writeICS) {
  498.             if (refreshAction) {
  499.                 // reschedule the refresh for next round, after the file has been written;
  500.                 // strictly we may not need to refresh once the file has been successfully
  501.                 // written, but we don't know if that write will succeed.
  502.                 this.queue.unshift(refreshAction);
  503.             }
  504.             this.writeICS();
  505.         }
  506.         else if (refreshAction) {
  507.             this.doRefresh();
  508.         }
  509.     },
  510.  
  511.     lock: function () {
  512.         this.locked = true;
  513.     },
  514.  
  515.     unlock: function () {
  516.         this.locked = false;
  517.         this.processQueue();
  518.     },
  519.     
  520.     isLocked: function () {
  521.         return this.locked;
  522.     },
  523.  
  524.     startBatch: function ()
  525.     {
  526.         this.mObserver.onStartBatch();
  527.     },
  528.     endBatch: function ()
  529.     {
  530.         this.mObserver.onEndBatch();
  531.     },
  532.  
  533.     // nsIInterfaceRequestor impl
  534.     getInterface: function(iid, instance) {
  535.         if (iid.equals(Components.interfaces.nsIAuthPrompt)) {
  536.             return new calAuthPrompt();
  537.         }
  538.         else if (iid.equals(Components.interfaces.nsIPrompt)) {
  539.             // use the window watcher service to get a nsIPrompt impl
  540.             return Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
  541.                              .getService(Components.interfaces.nsIWindowWatcher)
  542.                              .getNewPrompter(null);
  543.         }
  544.         Components.returnCode = Components.results.NS_ERROR_NO_INTERFACE;
  545.         return null;
  546.     },
  547.  
  548.     /**
  549.      * Make a backup of the (remote) calendar
  550.      *
  551.      * This will download the remote file into the profile dir.
  552.      * It should be called before every upload, so every change can be
  553.      * restored. By default, it will keep 3 backups. It also keeps one
  554.      * file each day, for 3 days. That way, even if the user doesn't notice
  555.      * the remote calendar has become corrupted, he will still loose max 1
  556.      * day of work.
  557.      * After the back up is finished, will call aCallback.
  558.      *
  559.      * @param aCallback
  560.      *           Function that will be calles after the backup is finished.
  561.      *           will be called in the original context in which makeBackup
  562.      *           was called
  563.      */
  564.     makeBackup: function(aCallback) {
  565.         // Uses |pseudoID|, an id of the calendar, defined below
  566.         function makeName(type) {
  567.             return 'calBackupData_'+pseudoID+'_'+type+'.ics';
  568.         }
  569.         
  570.         // This is a bit messy. createUnique creates an empty file,
  571.         // but we don't use that file. All we want is a filename, to be used
  572.         // in the call to copyTo later. So we create a file, get the filename,
  573.         // and never use the file again, but write over it.
  574.         // Using createUnique anyway, because I don't feel like 
  575.         // re-implementing it
  576.         function makeDailyFileName() {
  577.             var dailyBackupFile = backupDir.clone();
  578.             dailyBackupFile.append(makeName('day'));
  579.             dailyBackupFile.createUnique(CI.nsIFile.NORMAL_FILE_TYPE, 0600);
  580.             dailyBackupFileName = dailyBackupFile.leafName;
  581.  
  582.             // Remove the reference to the nsIFile, because we need to
  583.             // write over the file later, and you never know what happens
  584.             // if something still has a reference.
  585.             // Also makes it explicit that we don't need the file itself,
  586.             // just the name.
  587.             dailyBackupFile = null;
  588.  
  589.             return dailyBackupFileName;
  590.         }
  591.  
  592.         function purgeBackupsByType(files, type) {
  593.             // filter out backups of the type we care about.
  594.             var filteredFiles = files.filter(
  595.                 function f(v) { 
  596.                     return (v.name.indexOf("calBackupData_"+pseudoID+"_"+type) != -1)
  597.                 });
  598.             // Sort by lastmodifed
  599.             filteredFiles.sort(
  600.                 function s(a,b) {
  601.                     return (a.lastmodified - b.lastmodified);
  602.                 });
  603.             // And delete the oldest files, and keep the desired number of
  604.             // old backups
  605.             var i;
  606.             for (i = 0; i < filteredFiles.length - numBackupFiles; ++i) {
  607.                 file = backupDir.clone();
  608.                 file.append(filteredFiles[i].name);
  609.  
  610.                 // This can fail because of some crappy code in nsILocalFile.
  611.                 // That's not the end of the world.  We can try to remove the
  612.                 // file the next time around.
  613.                 try {
  614.                     file.remove(false);
  615.                 } catch(ex) {}
  616.             }
  617.             return;
  618.         }
  619.  
  620.         function purgeOldBackups() {
  621.             // Enumerate files in the backupdir for expiry of old backups
  622.             var dirEnum = backupDir.directoryEntries;
  623.             var files = [];
  624.             while (dirEnum.hasMoreElements()) {
  625.                 var file = dirEnum.getNext().QueryInterface(CI.nsIFile);
  626.                 if (file.isFile()) {
  627.                     files.push({name: file.leafName, lastmodified: file.lastModifiedTime});
  628.                 }
  629.             }
  630.  
  631.             if (doDailyBackup)
  632.                 purgeBackupsByType(files, 'day');
  633.             else
  634.                 purgeBackupsByType(files, 'edit');
  635.  
  636.             return;
  637.         }
  638.         
  639.         function copyToOverwriting(oldFile, newParentDir, newName) {
  640.             try {
  641.                 var newFile = newParentDir.clone();
  642.                 newFile.append(newName);
  643.             
  644.                 if (newFile.exists()) {
  645.                     newFile.remove(false);
  646.                 }
  647.                 oldFile.copyTo(newParentDir, newName);
  648.             } catch(e) {
  649.                 Components.utils.reportError("Backup failed, no copy:"+e);
  650.                 // Error in making a daily/initial backup.
  651.                 // not fatal, so just continue
  652.             }
  653.         }
  654.  
  655.         function getIntPrefSafe(prefName, defaultValue)
  656.         {
  657.             try {
  658.                 var prefValue = backupBranch.getIntPref(prefName);
  659.                 return prefValue;
  660.             }
  661.             catch (ex) {
  662.                 return defaultValue;
  663.             }
  664.         }
  665.         var backupDays = getIntPrefSafe("days", 1);
  666.         var numBackupFiles = getIntPrefSafe("filenum", 3);
  667.  
  668.         try {
  669.             var dirService = Components.classes["@mozilla.org/file/directory_service;1"]
  670.                                        .getService(CI.nsIProperties);
  671.             var backupDir = dirService.get("ProfD", CI.nsILocalFile);
  672.             backupDir.append("backupData");
  673.             if (!backupDir.exists()) {
  674.                 backupDir.create(CI.nsIFile.DIRECTORY_TYPE, 0755);
  675.             }
  676.         } catch(e) {
  677.             // Backup dir wasn't found. Likely because we are running in
  678.             // xpcshell. Don't die, but continue the upload.
  679.             LOG("Backup failed, no backupdir:"+e);
  680.             aCallback.call(this);
  681.             return;
  682.         }
  683.  
  684.         try {
  685.             var pseudoID = getCalendarManager().getCalendarPref(this, "UNIQUENUM");
  686.             if (!pseudoID) {
  687.                 pseudoID = new Date().getTime();
  688.                 getCalendarManager().setCalendarPref(this, "UNIQUENUM", pseudoID);
  689.             }
  690.         } catch(e) {
  691.             // calendarmgr not found. Likely because we are running in
  692.             // xpcshell. Don't die, but continue the upload.
  693.             LOG("Backup failed, no calendarmanager:"+e);
  694.             aCallback.call(this);
  695.             return;
  696.         }
  697.  
  698.         var doInitialBackup = false;
  699.         var initialBackupFile = backupDir.clone();
  700.         initialBackupFile.append(makeName('initial'));
  701.         if (!initialBackupFile.exists())
  702.             doInitialBackup = true;
  703.  
  704.         var doDailyBackup = false;
  705.         var backupTime = new Number(getCalendarManager().
  706.                                        getCalendarPref(this, 'backup-time'));
  707.         if (!backupTime ||
  708.             (new Date().getTime() > backupTime + backupDays*24*60*60*1000)) {
  709.             // It's time do to a daily backup
  710.             doDailyBackup = true;
  711.             getCalendarManager().setCalendarPref(this, 'backup-time', 
  712.                                                  new Date().getTime());
  713.         }
  714.  
  715.         var dailyBackupFileName;
  716.         if (doDailyBackup) {
  717.             dailyBackupFileName = makeDailyFileName(backupDir);
  718.         }
  719.  
  720.         var backupFile = backupDir.clone();
  721.         backupFile.append(makeName('edit'));
  722.         backupFile.createUnique(CI.nsIFile.NORMAL_FILE_TYPE, 0600);
  723.         
  724.         purgeOldBackups();
  725.  
  726.         // Now go download the remote file, and store it somewhere local.
  727.         var ioService = Components.classes["@mozilla.org/network/io-service;1"]
  728.                                   .getService(CI.nsIIOService);
  729.         var channel = ioService.newChannelFromURI(this.mUri);
  730.         channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
  731.         channel.notificationCallbacks = this;
  732.  
  733.         var downloader = Components.classes["@mozilla.org/network/downloader;1"]
  734.                                    .createInstance(CI.nsIDownloader);
  735.  
  736.         var savedthis = this;
  737.         var listener = {
  738.             onDownloadComplete: function(downloader, request, ctxt, status, result) {
  739.                 if (doInitialBackup)
  740.                     copyToOverwriting(result, backupDir, makeName('initial'));
  741.                 if (doDailyBackup)
  742.                     copyToOverwriting(result, backupDir, dailyBackupFileName);
  743.  
  744.                 aCallback.call(savedthis);
  745.             },
  746.         }
  747.  
  748.         downloader.init(listener, backupFile);
  749.         try {
  750.             channel.asyncOpen(downloader, null);
  751.         } catch(e) {
  752.             // For local files, asyncOpen throws on new (calendar) files
  753.             // No problem, go and upload something
  754.             LOG("Backup failed in asyncOpen:"+e);
  755.             aCallback.call(this);
  756.             return;
  757.         }
  758.  
  759.         return;
  760.     }
  761. };
  762.  
  763. function calICSObserver(aCalendar) {
  764.     this.mCalendar = aCalendar;
  765.     this.mObservers = new calListenerBag(Components.interfaces.calIObserver);
  766. }
  767.  
  768. calICSObserver.prototype = {
  769.     mCalendar: null,
  770.     mObservers: null,
  771.     mInBatch: false,
  772.  
  773.     // calIObserver:
  774.     onStartBatch: function() {
  775.         this.mObservers.notify("onStartBatch");
  776.         this.mInBatch = true;
  777.     },
  778.     onEndBatch: function() {
  779.         this.mObservers.notify("onEndBatch");
  780.         this.mInBatch = false;
  781.     },
  782.     onLoad: function(calendar) {
  783.         this.mObservers.notify("onLoad", [calendar]);
  784.     },
  785.     onAddItem: function(aItem) {
  786.         this.mObservers.notify("onAddItem", [aItem]);
  787.     },
  788.     onModifyItem: function(aNewItem, aOldItem) {
  789.         this.mObservers.notify("onModifyItem", [aNewItem, aOldItem]);
  790.     },
  791.     onDeleteItem: function(aDeletedItem) {
  792.         this.mObservers.notify("onDeleteItem", [aDeletedItem]);
  793.     },
  794.  
  795.     // Unless an error number is in this array, we consider it very bad, set
  796.     // the calendar to readOnly, and give up.
  797.     acceptableErrorNums: [],
  798.  
  799.     onError: function(aErrNo, aMessage) {
  800.         var errorIsOk = false;
  801.         for each (num in this.acceptableErrorNums) {
  802.             if (num == aErrNo) {
  803.                 errorIsOk = true;
  804.                 break;
  805.             }
  806.         }
  807.         if (!errorIsOk)
  808.             this.mCalendar.readOnly = true;
  809.         this.mObservers.notify("onError", [aErrNo, aMessage]);
  810.     },
  811.  
  812.     // This observer functions as proxy for all the other observers
  813.     // So need addObserver and removeObserver here
  814.     addObserver: function (aObserver) {
  815.         this.mObservers.add(aObserver);
  816.     },
  817.  
  818.     removeObserver: function (aObserver) {
  819.         this.mObservers.remove(aObserver);
  820.     }
  821. };
  822.  
  823. /***************************
  824.  * Transport Abstraction Hooks
  825.  *
  826.  * Those hooks provide a way to do checks before or after publishing an
  827.  * ics file. The main use will be to check etags (or some other way to check
  828.  * for remote changes) to protect remote changes from being overwritten.
  829.  *
  830.  * Different protocols need different checks (webdav can do etag, but
  831.  * local files need last-modified stamps), hence different hooks for each
  832.  * types
  833.  */
  834.  
  835. // dummyHooks are for transport types that don't have hooks of their own.
  836. // Also serves as poor-mans interface definition.
  837. function dummyHooks() {
  838. }
  839.  
  840. dummyHooks.prototype = {
  841.     onBeforeGet: function(aChannel) {
  842.         return true;
  843.     },
  844.     
  845.     /**
  846.      * @return
  847.      *     a boolean, false if the previous data should be used (the datastore
  848.      *     didn't change, there might be no data in this GET), true in all
  849.      *     other cases
  850.      */
  851.     onAfterGet: function() {
  852.         return true;
  853.     },
  854.  
  855.     onBeforePut: function(aChannel) {
  856.         return true;
  857.     },
  858.     
  859.     onAfterPut: function(aChannel) {
  860.         return true;
  861.     },
  862. }
  863.  
  864. function httpHooks() {
  865.     this.mChannel = null;
  866. }
  867.  
  868. httpHooks.prototype = {
  869.     onBeforeGet: function(aChannel) {
  870.         this.mChannel = aChannel;
  871.         if (this.mEtag) {
  872.             var httpchannel = aChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
  873.             // Somehow the webdav header 'If' doesn't work on apache when
  874.             // passing in a Not, so use the http version here.
  875.             httpchannel.setRequestHeader("If-None-Match", this.mEtag, false);
  876.         }
  877.  
  878.         return true;
  879.     },
  880.     
  881.     onAfterGet: function() {
  882.         var httpchannel = this.mChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
  883.  
  884.         // 304: Not Modified
  885.         // Can use the old data, so tell the caller that it can skip parsing.
  886.         if (httpchannel.responseStatus == 304)
  887.             return false;
  888.  
  889.         // 404: Not Found
  890.         // This is a new calendar. Shouldn't try to parse it. But it also
  891.         // isn't a failure, so don't throw.
  892.         if (httpchannel.responseStatus == 404)
  893.             return false;
  894.  
  895.         try {
  896.             this.mEtag = httpchannel.getResponseHeader("ETag");
  897.         } catch(e) {
  898.             // No etag header. Now what?
  899.             this.mEtag = null;
  900.         }
  901.         this.mChannel = null;
  902.         return true;
  903.     },
  904.  
  905.     onBeforePut: function(aChannel) {
  906.         if (this.mEtag) {
  907.             var httpchannel = aChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
  908.  
  909.             // Apache doesn't work correctly with if-match on a PUT method,
  910.             // so use the webdav header
  911.             httpchannel.setRequestHeader("If", '(['+this.mEtag+'])', false);
  912.         }
  913.         return true;
  914.     },
  915.     
  916.     onAfterPut: function(aChannel) {
  917.         var httpchannel = aChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
  918.         try {
  919.             this.mEtag = httpchannel.getResponseHeader("ETag");
  920.         } catch(e) {
  921.             // There was no ETag header on the response. This means that
  922.             // putting is not atomic. This is bad. Race conditions can happen,
  923.             // because there is a time in which we don't know the right
  924.             // etag.
  925.             // Try to do the best we can, by immediatly getting the etag.
  926.             
  927.             // Only on branch, because webdav doesn't work on trunk: bug 332840
  928.             if (isOnBranch) {
  929.                 var res = new WebDavResource(aChannel.URI);
  930.                 var webSvc = Components.classes['@mozilla.org/webdav/service;1']
  931.                                        .getService(Components.interfaces.nsIWebDAVService);
  932.                 // The namespace is 'DAV:', not just 'DAV'.
  933.                 webSvc.getResourceProperties(res, 1, ['DAV: getetag'], false,
  934.                                              this, null, null);
  935.             } else {
  936.                 // instead, on trunk, set mEtag to null, so it will be ignored on 
  937.                 // the next GET/PUT
  938.                 this.mEtag = null;
  939.             }
  940.         }
  941.         return true;
  942.     },
  943.  
  944.     onOperationComplete: function(aStatusCode, aResource, aOperation, aClosure) {
  945.     },
  946.  
  947.     onOperationDetail: function(aStatusCode, aResource, aOperation, aDetail, aClosure) {
  948.         var props = aDetail.QueryInterface(Components.interfaces.nsIProperties);
  949.         try {
  950.             this.mEtag = props.get('DAV: getetag', Components.interfaces.nsISupportsString).toString();
  951.         } catch(e) {
  952.             // No etag header. Now what?
  953.             this.mEtag = null;
  954.         }
  955.     },
  956. }
  957.  
  958. function WebDavResource(url) {
  959.     this.mResourceURL = url;
  960. }
  961.  
  962. WebDavResource.prototype = {
  963.     mResourceURL: {},
  964.     get resourceURL() { return this.mResourceURL; },
  965.     QueryInterface: function(iid) {
  966.         if (iid.equals(CI.nsIWebDAVResource) ||
  967.             iid.equals(CI.nsISupports)) {
  968.             return this;
  969.         }
  970.         throw Components.interfaces.NS_ERROR_NO_INTERFACE;
  971.     }
  972. };
  973.  
  974. /****
  975.  **** module registration
  976.  ****/
  977.  
  978. var calICSCalendarModule = {
  979.  
  980.     mCID: Components.ID("{f8438bff-a3c9-4ed5-b23f-2663b5469abf}"),
  981.     mContractID: "@mozilla.org/calendar/calendar;1?type=ics",
  982.  
  983.     mUtilsLoaded: false,
  984.     loadUtils: function icsLoadUtils() {
  985.         if (this.mUtilsLoaded)
  986.             return;
  987.  
  988.         const jssslContractID = "@mozilla.org/moz/jssubscript-loader;1";
  989.         const jssslIID = Components.interfaces.mozIJSSubScriptLoader;
  990.  
  991.         const iosvcContractID = "@mozilla.org/network/io-service;1";
  992.         const iosvcIID = Components.interfaces.nsIIOService;
  993.  
  994.         var loader = Components.classes[jssslContractID].getService(jssslIID);
  995.         var iosvc = Components.classes[iosvcContractID].getService(iosvcIID);
  996.  
  997.         // Note that unintuitively, __LOCATION__.parent == .
  998.         // We expect to find utils in ./../js
  999.         var appdir = __LOCATION__.parent.parent;
  1000.         appdir.append("js");
  1001.         var scriptName = "calUtils.js";
  1002.  
  1003.         var f = appdir.clone();
  1004.         f.append(scriptName);
  1005.  
  1006.         try {
  1007.             var fileurl = iosvc.newFileURI(f);
  1008.             loader.loadSubScript(fileurl.spec, this.__parent__.__parent__);
  1009.         } catch (e) {
  1010.             dump("Error while loading " + fileurl.spec + "\n");
  1011.             throw e;
  1012.         }
  1013.  
  1014.         this.mUtilsLoaded = true;
  1015.     },
  1016.     
  1017.     registerSelf: function (compMgr, fileSpec, location, type) {
  1018.         compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
  1019.         compMgr.registerFactoryLocation(this.mCID,
  1020.                                         "Calendar ICS provider",
  1021.                                         this.mContractID,
  1022.                                         fileSpec,
  1023.                                         location,
  1024.                                         type);
  1025.     },
  1026.  
  1027.     getClassObject: function (compMgr, cid, iid) {
  1028.         if (!cid.equals(this.mCID))
  1029.             throw Components.results.NS_ERROR_NO_INTERFACE;
  1030.  
  1031.         if (!iid.equals(Components.interfaces.nsIFactory))
  1032.             throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  1033.  
  1034.         this.loadUtils();
  1035.  
  1036.         return this.mFactory;
  1037.     },
  1038.  
  1039.     mFactory: {
  1040.         createInstance: function (outer, iid) {
  1041.             if (outer != null)
  1042.                 throw Components.results.NS_ERROR_NO_AGGREGATION;
  1043.             return (new calICSCalendar()).QueryInterface(iid);
  1044.         }
  1045.     },
  1046.  
  1047.     canUnload: function(compMgr) {
  1048.         return true;
  1049.     }
  1050. };
  1051.  
  1052. function NSGetModule(compMgr, fileSpec) {
  1053.     return calICSCalendarModule;
  1054. }
  1055.